前言

在上一篇文章 Android NDK 入门 - 初探 CMake 中,学习了如何在 AS 中使用 CMake 来开发 NDK,而编写 CMakeLists.txt 构建脚本是其中一个重要的环节,今天我们就来一起学习 CMakeLists.txt 的一些应用,介绍它在下面四种场景的用法:

  • CMakeLists.txt 文件解析
  • 使用 Android NDK 的 API
  • 编写 so 库
  • 使用 so 库

CMakeLists.txt 文件解析

把常用的语法说明下,关于 CMake 的语法,可以查看 官方的 API 说明

  • cmake_minimum_required(VERSION3.4.1)
    指定 CMake 最低版本

  • add_library(native-lib SHARED src/main/cpp/native-lib.cpp)
    用于向 CMake 添加依赖源文件或库,指令需传入三个参数(函数库名称、库类型、依赖源文件相对路径)

    • 函数库名称:生成函数库的名称,决定了最终生成的共享库的名字,例如我们将共享库的名字定义为 native-lib ,那么最终生成的 so 文件将在前面加上 lib 前缀即 libnative-lib.so 或 libnative-lib.a ,但是我们在代码中加载该共享库的时候,仍然应当使用 native-lib ,也就是像下面这样

      static {
      System.loadLibrary(“native-lib”);
      }
    • 库类型:动态库为 SHARED ,静态库为 STATIC,我们可以指定根据源文件编译出来的是静态库还是共享库,这里简单提一下两者的区别:

      • 静态库:以.a结尾。静态库在程序链接的时候使用,链接器会将程序中使用到函数的代码从库文件中拷贝到应用程序中。一旦链接完成,在执行程序的时候就不需要静态库了。
      • 共享库:以.so结尾。在程序的链接时候并不像静态库那样在拷贝使用函数的代码,而只是作些标记。然后在程序开始启动运行的时候,动态地加载所需模块。
    • 依赖源文件相对路径,依赖的 c/cpp 文件(相对路径),如果我们有多个源文件,那么就在后面添加文件的路径即可。

  • find_library(log-lib log)

    用于定位 NDK 中的库 ,需传入两个参数(path变量、ndk 库名称) ,具体有哪些 API 查看官网

    • path变量:设置 path 变量的名称,这里为 NDK 中的日志库,文件位于 $NDK/platforms/android-<level>/<abi>/usr/lib
    • ndk 库名称:指定 cmake 查询库的名称,即在 ndk 开发包中查询 liblog.so 函数库,将其路径赋值给 log-lib
  • target_link_libraries(native-lib source1 source2 ... sourceN)

    要将预构建库关联到您自己的原生库,需传入至少两个参数(指定目标库、链接的库)

    • 指定目标库:与上面 add_library 指定的函数库名一致
    • 链接的库:可链接 add_library ,find_library 中的库,其中 find_library 的库要填写变量 ${log-lib}
  • include_directories(src/main/cpp/include/)

    为了确保 CMake 可以在编译时定位您的标头文件,填写头文件路径

使用 Android NDK 的 API

在 Android 系统当中,预制了一些标准的 NDK 库,这些库函数的目的就是让开发者能够在原生方法中实现之前在 Java 层开发的一些功能,我们可以通过 NDK 库 查找所需要的 API 。

因为这些库已经预制在系统当中了,所以如果我们要调用这些库中的函数,那么不需要将其打包到 APK 当中,所需要做的就是向 CMake 提供希望使用的库名称,并将其关联到自己的原生库,最后在原生代码中引入相应的头文件,调用方法就可以了。

下面,我们再来创建一个新项目(Including C++ support),研究 NDK 的 API。

Snipaste_2018-03-17_14-13-23.png
Snipaste_2018-03-17_14-13-23.png

创建项目后,打开 CMakeLists.txt ,看到引用了 NDK 的 log 库,并链接到 native-lib

Snipaste_2018-03-17_14-21-21.png
Snipaste_2018-03-17_14-21-21.png

Snipaste_2018-03-17_14-22-45.png

既然引用了 log 库,那我们就在 native-lib 里调用 log 库

在 MainActivity 里定义 native 接口,按 ALT+ENTER 自动生成 C 函数

public native void printByJNI(String tag, String content);

编写 printByJNI 函数

Snipaste_2018-03-17_14-33-19.png

最后在 Activity 里调用 printByJNI(TAG, "onCreate()"); ,查看结果。本来打算再引用个 NDK 的 API,但代码很复杂 ,倒不如自己写个简单的 so 库,再玩玩 CMakeLists 的配置 。

编写 so 库

观察上面的 log 库,发现引入一个 so 库需要 libxxx.so 文件和 .h 文件。OK,编写一个简单的计算器 so 库。

在 cpp 目录下创建 calc-lib ,注意要同时生成 .cpp 和 .h 文件

Snipaste_2018-03-17_14-57-20.png
Snipaste_2018-03-17_14-57-20.png

在 cpp 目录下创建 include 文件夹,把 .h 文件放进去

Snipaste_2018-03-17_15-32-42.png
Snipaste_2018-03-17_15-32-42.png

编写 calc-lib.h 头文件,定义方法

#ifndef CMAKEDEMO2_CALC_LIB_H
#define CMAKEDEMO2_CALC_LIB_H

int add(int x, int y);

int sub(int x, int y);

#endif

编写 calc-lib.cpp 文件,实现方法

#include "include/calc-lib.h"

int add(int x, int y) {
return x + y;
}

int sub(int x, int y) {
return x - y;
}

编写完 calc-lib 库后,要修改 CMakeLists ,然后我们再通过 JNI 测试下我们的库是否能用

Snipaste_2018-03-17_15-39-06.png
Snipaste_2018-03-17_15-39-06.png
1. add_library 增加一个库,这个上面已经说过
2. include_directories 为了确保 CMake 可以在编译时定位您的标头文件
3. target_link_libraries 我们想通过 native-lib 调用 calc-lib,所以链接,注意可以同时链接多个库

每次修改 CMakeLists 都需要执行 Refresh Linked C++ ProjectClean Project,等待 gradle 刷新完

Snipaste_2018-03-17_17-01-11.png
Snipaste_2018-03-17_17-01-11.png

编写 Java 接口,然后生成 JNI 函数,再调用我们的 calc-lib 库,基本上就这样。

Snipaste_2018-03-17_15-50-04.png
Snipaste_2018-03-17_15-50-04.png

Snipaste_2018-03-17_15-52-08.png

最后启动 App,查看结果。

使用 so 库

经过上面的折腾,我们实现了在同个 App 内调用我们编写的 so 库,那怎么让别人使用我们的 so 库,同时不公开代码呢(主要实现代码在 calc-lib.cpp 内)

继续折腾,把 so 库提取出来,导入到项目 ,然后删除 calc-lib.cpp,最后通过 native-lib 调用。

Build APK 后,在 app/build/intermediates/cmake 下会生成 so 库,我们把 libcalc-lib.so 拷贝出来

Snipaste_2018-03-17_16-21-13.png
Snipaste_2018-03-17_16-21-13.png

app/src/main/ 下创建 jniLibs 文件夹,把 so 库复制进去

Snipaste_2018-03-17_16-25-47.png
Snipaste_2018-03-17_16-25-47.png

删除 app/src/main/cpp/calc-lib.cpp

Snipaste_2018-03-17_17-06-32.png
Snipaste_2018-03-17_17-06-32.png

修改 CMakeLists

Snipaste_2018-03-17_16-55-50.png

1. add_library 通过 IMPORTANT 标志告知 CMake 只希望将库导入到项目中
2. set_target_properties ${CMAKE_SOURCE_DIR}表示的是 CMakeLists.txt 所在的路径,我们指定第三方 so 所在路径时,应当以这个常量为起点。
按理来说,我们应当为每种 ABI 接口提供单独的软件包,那么,我们就可以在 jinLibs 下建立多个文件夹,每个文件夹对应一种 ABI 接口类型,之后再通过 ${ANDROID_ABI} 来泛化这一层目录的结构,这样将有助于充分利用特定的CPU架构

执行 Refresh Linked C++ ProjectClean Project,等待 gradle 刷新完,最后启动 App,查看结果。

小结

这篇文章简单的解释 CMakeLists 文件的配置,如何导入 Android NDK 的 API,如何链接 so 库到 JNI,如何编写和使用 so 库,接下来会继续学习 ndk-build 的方式开发 Android NDK,对比两种方式。

参考:

https://www.jianshu.com/p/843cf09a1db2